﻿using System;
using System.Net;
using System.Collections.Generic;
using SCG.General;
using System.IO;

namespace SCG.SolarVengeanceEngine
{
    public abstract class Scenario
    {
        //constructor - call initialize
        public Scenario()
        {            
        }

        //The BuildScenario method must be overridden to create the scenario
        public abstract void BuildScenario(int numPlayers, int capitalValues, int startingResources);

        //Derived class must return the maximum number of players the scenario allows
        public virtual int MaxPlayers
        {
            get
            {
                return 10;
            }
        }

        //Access the Scenario's name
        public string Name
        {
            get
            {
                return GetType().Name;
            }
        }     

        //display name
        public string DisplayName
        {
            get
            {
                string s = Name;
                string outName = "";
                foreach(char c in s)
                {
                    if (c >= 'A' && c <= 'Z')
                        if (outName != "")
                            outName += " ";
                    outName += c;
                }
                return outName;
            }
        }

        //Description property
        public abstract string Description
        {
            get;
        }

        //Author
        public virtual string Author
        {
            get
            {
                return "SCG";
            }
        }

        //The size of the map
        public virtual int MapWidth
        {
            get
            {
                return 100;
            }
        }
        public virtual int MapHeight
        {
            get
            {
                return 100;
            }
        }

        //Recommended play mode
        public virtual PlayModeRecommendation Recommendation
        {
            get
            {
                return PlayModeRecommendation.Both;
            }
        }

        //AutoBuild mode
        public virtual AutoBuildMode AutoBuildMode
        {
            get
            {
                return AutoBuildMode.FillAll;
            }
        }

        //does the scenario use capitals?
        public virtual bool UsesCapitals
        {
            get
            {
                return true;
            }
        }

        //The default starting value of capitals
        public virtual int DefaultCapitalValue
        {
            get
            {
                return 20;
            }
        }

        //The default starting resources at capitals
        public virtual int DefaultCapitalResources
        {
            get
            {
                return 100;
            }
        }

        //Default capital shields
        public virtual int DefaultCapitalShields
        {
            get
            {
                return 10;
            }
        }

        //Default capital scanners
        public virtual int DefaultCapitalScanners
        {
            get
            {
                return 10;
            }
        }

        //Access the list of defined parameters       
        public List<ScenarioParameter> Parameters
        {
            get
            {
                return _parameters;
            }
        }

        //Return a list of StarShip types that are dis-allowed
        public virtual IList<StarShipType> DisallowStarShipTypes
        {
            get
            {
                return _empty;
            }
        }

        //Return a list of StarShip types that are added
        public virtual IList<StarShipType> AddedStarShipTypes
        {
            get
            {
                return _empty;
            }
        }

        //process a list of starship types to add/remove
        public void ProcessStarShipTypes(List<StarShipType> lst)
        {
            foreach (StarShipType st in AddedStarShipTypes)
                if (!lst.Contains(st))
                    lst.Add(st);
            foreach (StarShipType st in DisallowStarShipTypes)
                if (lst.Contains(st))
                    lst.Remove(st);
        }

        //Access the Game instance
        public SVGame Game
        {
            get
            {
                return _game;
            }
            set
            {
                _game = value;
            }
        }

        //access the dynamic events interface
        public IDynamicEvents DynamicEvents
        {
            get
            {
                return Game as IDynamicEvents;
            }
        }

        //Control whether capitals are assigned to players
        public virtual bool AssignCapitals
        {
            get
            {
                return true;
            }
        }

        //Apply standard capital elimination logic
        public virtual bool CapitalElimination
        {
            get
            {
                return true;
            }
        }

        //Initialize and kick off the Build process
        public void BuildScenaio(SVGame game, Confluxer nameGenerator, int numPlayers, int capitalValues, int startingResources, ITerrainImageMapper mapper)
        {
            //Assign references to objects
            _game = game;
            _game.Scenario = this;
            _nameGenerator = nameGenerator;
            if (_nameGenerator != null)
                _nameGenerator.Reset();
            _mapper = mapper;
            if (numPlayers > MaxPlayers)
                numPlayers = MaxPlayers;

            //Create the players
            for (int p = 0; p < numPlayers; p++)
            {
                Player player = new Player(_game);
                if (_nameGenerator != null)
                    player.Name = "Lord " + _nameGenerator.RandomName;
            }

            //Allocate the map
            game.SetDimensions(MapWidth, MapHeight);

            //Build the scenario - give it a few tries
            Exception ex = null;
            int buildTries = 10;
            do
            {
                try
                {
                    ex = null;
                    BuildScenario(numPlayers, capitalValues, startingResources);
                    buildTries = 0;
                }
                catch (FileNotFoundException exp)
                {
                    throw new Exception("File not found " + exp.Message);
                }
                catch (Exception exp)
                {
                    ex = exp;
                    buildTries--;
                }
            }
            while (buildTries > 0);

            //Re-throw exception
            if (ex != null)
                throw ex;

            //Must have enough StarSystems
            if (game.StarSystems.Count < numPlayers && AssignCapitals)
                throw new InvalidOperationException("Not enough StarSystems generated");

            //Remove map objects outside bounds
            for (int i = _game.MapObjects.Count - 1; i >= 0; i--)
            {
                SVMapObject mo = _game.MapObjects[i];
                if (!_game.InBounds(mo.X, mo.Y))
                {
                    mo.Remove();
                    if (mo is StarSystem)
                    {
                        StarSystem sys = mo as StarSystem;
                        if (sys.IsCapital)
                            sys.Owner.Capital = null;
                    }
                }
            }

            //If any players do not have capitals, assign them now
            if (AssignCapitals)
            {
                foreach (Player player in game.Players)
                {
                    if (player.Capital == null)
                    {
                        //First attempt, locate one at least 5 spaces away from anything else
                        int tries = 2000;
                        StarSystem capital = null;
                        do
                        {
                            tries--;
                            capital = game.StarSystems[SVGame.RNG.Next(game.StarSystems.Count)];
                            if (capital.Owner == null)
                                if (CapitalLocationOK(capital, 10))
                                {
                                    player.Capital = capital;
                                    capital.Owner = player;
                                    break;
                                }
                        }
                        while (tries > 0);

                        //Second attempt, take the first available one
                        if (player.Capital == null)
                            foreach (StarSystem system in game.StarSystems)
                                if (system.Owner == null)
                                {
                                    capital = system;
                                    break;
                                }

                        //Assign it as the player's capital
                        player.Capital = capital;
                        capital.Owner = player;
                    }
                }
            }

            //set the capital values
            foreach (Player player in game.Players)
                if (player.Capital != null)
                {
                    StarSystem capital = player.Capital;
                    capital.Value = capitalValues;
                    capital.Resources = startingResources;
                    capital.Shields = DefaultCapitalShields;
                    capital.Scanners = DefaultCapitalScanners;
                }

            //make sure current player and visibility is established
            if (game.Players.Count == 0)
            {
                Player p = new Player(game);
                p.Name = "Player 1";
            }    
        }

        //custom rendering
        public virtual void CustomRendering(ISVCustomRenderer renderer)
        {
        }

        //Access the Name Generator
        public Confluxer NameGenerator
        {
            get
            {
                return _nameGenerator;
            }
        }

        //The Scenario API
        #region ScenarioApi

        //Allow scenarios to override to define their own victory conditions
        public virtual bool VictoryReached()
        {
            int defeated = 0;
            List<Player> playersLeft = new List<Player>();
            foreach (Player player in Game.Players)
                if (player.Eliminated)
                    defeated++;
                else
                    playersLeft.Add(player);

            //Don't declare a victory if there were no players ever defeated
            if (defeated == 0)
                return false;

            //Victory is reached if all of the remaining players are mutual allies
            bool victory = true;
            foreach (Player p1 in playersLeft)
                foreach (Player p2 in playersLeft)
                    if (p1 != p2)
                        if (p1.AlliedWith(p2) < AllianceLevel.Formal)
                        {
                            victory = false;
                            break;
                        }

            if (victory)
            {
                foreach (Player player in playersLeft)
                    player.Victor = true;
                return true;
            }
            else
                return false;
        }

        //Initialize support
        public virtual void ClientInitialize()
        {
        }

        //Dynamic processing
        public virtual void ProcessImpulse(int impulse)
        {
        }

        //executes each impulse for both hosts and clients
        public virtual void ClientProcessImpulse(int impulse)
        {
        }

        //Attempt to assign the player's capital within the specified area
        protected bool AssignCapital(Player player, SCGRect area)
        {
            if (_game.StarSystems.Count == 0)
                return false;
            if (player.Capital != null)
            {
                player.Capital.Owner = null;
                player.Capital = null;
            }
            int tries = 500;
            while (tries > 0)
            {
                int nbr = SVGame.RNG.Next(_game.StarSystems.Count);
                StarSystem test = _game.StarSystems[nbr];
                if (area.Contains(new SCGPoint(test.X, test.Y)))              
                    if (test.Owner == null)
                        if (CapitalLocationOK(test, 10))
                        {
                            player.Capital = test;
                            test.Owner = player;
                            return true;
                        }
                tries--;
            }
            return false;
        }

        //Create a Star Cluster
        protected void CreateStarCluster(int numStarSystems, int x1, int y1, int x2, int y2, int radius, int density)
        {
            int xDiff = x2 - x1;
            int yDiff = y2 - y1;
            double distance;
            int tries;
            int xAdd, yAdd;

            while (numStarSystems > 0)
            {
                //Create StarSystem
                StarSystem system = new StarSystem(_game);
                SetStarSystemValues(system);

                //Determine location
                tries = 0;
                int xTry;
                int yTry;
                do
                {
                    //get random point along cluster vector
                    distance = SVGame.RNG.NextDouble();
                    xTry = (int)(xDiff * distance + x1);
                    yTry = (int)(yDiff * distance + y1);

                    //apply radius distance
                    xAdd = 0;
                    yAdd = 0;
                    for (int c = 0; c < density; c++)
                    {
                        xAdd += (SVGame.RNG.Next(radius * 2) - radius);
                        yAdd += (SVGame.RNG.Next(radius * 2) - radius);
                    }
                    xAdd /= density;
                    yAdd /= density;
                    xTry += xAdd;
                    yTry += yAdd;

                    tries++;
                }
                while (!PositionOK(xTry, yTry, 2) && tries < 2000);
                numStarSystems--;

                //Remove the system and break if a valid location was not found
                if (PositionOK(xTry, yTry, 2))
                {
                    Game.SetToClosestEmpty(ref xTry, ref yTry);
                    system.SetLocation(xTry, yTry);
                }
                else
                {
                    system.Remove();
                    break;
                }
            }
        }

        //Create a StarSystem explicitly
        protected StarSystem CreateStarSystem(int x, int y)
        {
            _game.SetToClosestEmpty(ref x, ref y);
            StarSystem system = new StarSystem(_game);
            SetStarSystemValues(system);
            system.SetLocation(x, y);
            return system;
        }
        protected StarSystem CreateStarSystem(int x, int y, int value, int shields, int scanners, double resources)
        {
            _game.SetToClosestEmpty(ref x, ref y);
            StarSystem system = new StarSystem(_game);
            system.SetLocation(x, y);
            system.Value = value;
            system.Shields = shields;
            system.Scanners = scanners;
            system.Resources = resources;
            if (_nameGenerator != null)
                system.Name = _nameGenerator.RandomName;
            return system;
        }

        //Create a UDO
        protected UDO CreateUDO(string udoName, string imageFileName, int x, int y)
        {
            UDO udo = new UDO(_game, udoName, imageFileName);
            SCGPoint pt = _game.ClosestEmptyPoint(new SCGPoint(x, y));
            udo.SetLocation(pt.X, pt.Y);
            return udo;
        }

        //Create a StarShip
        protected StarShip CreateStarShip(int x, int y, Player owner, StarShipType shipType, int engines, int extra, bool cloaked)
        {
            _game.SetToClosestEmpty(ref x, ref y);
            StarShip ship = StarShip.CreateInstance(_game, owner, shipType, engines, extra, null);
            ship.Cloaked = cloaked;         
            ship.Code = Game.GetShipCode();
            ship.SetLocation(x, y);           
            return ship;
        }

        //Create a nebula
        protected void CreateNebula(int x, int y, int numArms, int numCells)
        {
            CreateTerrain(TerrainType.Nebula, x, y, numArms, numCells);
        }

        //Create terrain area
        protected void CreateTerrain(TerrainType tt, int x, int y, int numArms, int numCells)
        {
            for (int arm = 0; arm < numArms; arm++)
            {
                int startX = x;
                int startY = y;
                for (int cell = 0; cell < numCells; cell++)
                {
                    switch (SVGame.RNG.Next(4))
                    {
                        case 0:
                            startX++;
                            break;
                        case 1:
                            startX--;
                            break;
                        case 2:
                            startY++;
                            break;
                        case 3:
                            startY--;
                            break;
                    }
                    if (_game.InBounds(startX, startY))
                        _game.SetTerrain(startX, startY, _terrains[(int)tt]);
                }
            }
        }

        //Create a terrain based on an image
        protected ScenarioImage CreateTerrainImage(TerrainType tt, string imageFileName, int x, int y, double opacity)
        {
            //determine what to set the terrain to
            char terrain = _terrains[(int)tt];
            terrain = terrain.ToString().ToLower()[0];

            //create the ScenarioImage object and register it with the game
            ScenarioImage si = new ScenarioImage(Game, imageFileName, x, y, opacity);
            if (_mapper != null)
                _mapper.AssignImageObject(si);
            si.X = x;
            si.Y = y;         

            //map the terrain of the object
            if (tt != TerrainType.Clear && _mapper != null)
            {
                char[,] map = _mapper.GetTerrainImageMap(si, terrain);
                for (int tx = 0; tx < map.GetLength(0); tx++)
                    for (int ty = 0; ty < map.GetLength(1); ty++)
                        if (map[tx, ty] != ' ')
                            Game.SetTerrain(x + tx, y + ty, terrain);
            }

            return si;
        }

        //Create a BlackHole
        protected void CreateBlackHole(int x, int y, int radius)
        {
            _game.SetToClosestEmpty(ref x, ref y);
            BlackHole bh = new BlackHole(_game);
            bh.SetLocation(x, y);
            bh.Radius = radius;
        }

        //Create a Pulsar
        protected void CreatePulsar(int x, int y)
        {
            _game.SetToClosestEmpty(ref x, ref y);
            Pulsar ps = new Pulsar(_game);
            ps.SetLocation(x, y);
        }

        //Create a parameter that can modified from the setup screen
        protected ScenarioParameter CreateParameter(string name, int defaultValue, int minValue, int maxValue)
        {
            ScenarioParameter param = new ScenarioParameter();
            param.Name = name;
            param.Value = defaultValue;
            param.DefaultValue = defaultValue;
            param.MinValue = minValue;
            param.MaxValue = maxValue;
            _parameters.Add(param);
            return param;
        }

        //Create a wormhole
        protected void CreateWormhole(int x1, int y1, int x2, int y2)
        {
            _game.SetToClosestEmpty(ref x1, ref y1);
            Wormhole wh = new Wormhole(_game);
            wh.SetLocation(x1, y1);
            wh.DestX = x2;
            wh.DestY = y2;
        }

        //Get the terrain at the specifie cell
        protected TerrainType GetTerrain(int x, int y)
        {
            char t = _game.GetTerrain(x, y);
            if (t == ' ')
                return TerrainType.Clear;
            else
                return TerrainType.Nebula;
        }

        //Set the terrain at the cell to the specified type
        protected void SetTerrain(int x, int y, TerrainType terrain)
        {
            char t = ' ';
            if (terrain == TerrainType.Nebula)
                t = 'N';
            _game.SetTerrain(x, y, t);
        }

        #endregion

        //Allow scenario to persist information        
        protected void SetValue(string tag, string value)
        {
            _scenarioData[tag] = value;
            Game.SetScenarioData(tag, value);
        }        
        protected string GetValue(string tag)
        {
            if (_scenarioData.ContainsKey(tag))
                return _scenarioData[tag];
            else
                return "";
        }
        internal void AssignValueFromHost(string key, string value)
        {
            _scenarioData[key] = value;
        }

        //allow game to persist and restore scenario values
        internal string GetDataString()
        {
            string s = "";
            foreach (KeyValuePair<string, string> kvp in _scenarioData)
                s += kvp.Key + DELIM + kvp.Value + DELIM;
            return s;
        }
        internal void SetDataString(string data)
        {
            string[] tokens = data.Split(DELIM[0]);
            int t = 0;
            while (t < tokens.Length)
            {
                if (tokens[t].Trim() == "")
                    break;
                string key = tokens[t++];
                string value = tokens[t++];
                _scenarioData[key] = value;
            }
        }    

        //Helper Methods
        #region HelperMethods

        //get a random int within range
        protected int Random(int from, int to)
        {
            return SVGame.RNG.Next(to - from + 1) + from;
        }

        //General distance
        protected double Distance(double x1, double y1, double x2, double y2)
        {
            double diffX = (x1 - x2);
            double diffY = (y1 - y2);
            if (diffX == 0 && diffY == 0)
                return 0;
            return Math.Sqrt(diffX * diffX + diffY * diffY);
        }
        protected double DistanceQuick(double x1, double y1, double x2, double y2)
        {
            return Math.Abs(x1 - x2) + Math.Abs(y1 - y2);
        }

        #endregion

        //private members    
        private SVGame _game;
        private List<StarShipType> _empty = new List<StarShipType>();
        private Confluxer _nameGenerator;
        private char[] _terrains = { ' ', 'N' };
        private List<ScenarioParameter> _parameters = new List<ScenarioParameter>();
        internal static string DELIM = ((char)13).ToString();
        private Dictionary<string, string> _scenarioData = new Dictionary<string, string>();
        private ITerrainImageMapper _mapper = null;

        //Is this a valid position for a new StarSystem?
        private bool PositionOK(int x, int y, int minDistance)
        {
            if (x < 0 || x >= Game.Width || y < 0 || y >= Game.Height)
                return false;
            if (_game.GetObjectAt(x, y) != null)
                return false;
            foreach (SVMapObject svm in _game.MapObjects)
                if (svm.DistanceQuick(x, y) <= minDistance * 2)
                    if (svm.Distance(x, y) <= minDistance)
                        return false;
            return true;
        }

        //Is the specified system OK for a capital?
        private bool CapitalLocationOK(StarSystem capital, int minDistance)
        {
            foreach (StarSystem sys in Game.StarSystems)
            {
                if (sys == capital)
                    continue;
                if (sys.Owner == capital.Owner)
                    continue;
                if (sys.Owner == null)
                    continue;
                if (sys.DistanceQuick(capital) <= minDistance * 2)
                    if (sys.Distance(capital) <= minDistance)
                        return false;
            }
            return true;
        }

        //Set randomized system values
        private void SetStarSystemValues(StarSystem system)
        {
            if (_nameGenerator != null)
                system.Name = _nameGenerator.RandomName;

            //Randomize values
            system.Value = SVGame.RNG.Next(20) + 1;
            if (system.Value == 1)
                system.Value = SVGame.RNG.Next(20) + 1;
            system.Shields = SVGame.RNG.Next(system.Value);
            system.Scanners = (int)(SVGame.RNG.Next(system.Value) * 2.0d * SVGame.RNG.NextDouble());
            if (system.Scanners == 1)
                system.Scanners = 0;
            system.Resources = SVGame.RNG.Next(system.Value) * 3.0d * SVGame.RNG.NextDouble();

            //Assign a bonus
            if (SVGame.RNG.Next(100) > 90)
            {
                system.BonusValue = (int)(SVGame.RNG.Next(100) * SVGame.RNG.NextDouble());
                if (system.BonusValue < 5 || system.BonusValue > 90)
                    system.BonusValue = 0;
                system.BonusShipType = StarShip.ShipTypes[SVGame.RNG.Next(StarShip.ShipTypes.Count)];
            }
        }
    }

    //Terrain type
    public enum TerrainType { Clear = 0, Nebula };

    //Recommendation
    public enum PlayModeRecommendation { Solo = 1, MultiPlayer = 2, Both = Solo & MultiPlayer };

    //AutoBuild Mode
    public enum AutoBuildMode { FillAll, OneByOne };
}
